Ownership Rules
Ownership is Rust’s way of managing memory without a garbage collector and without runtime overhead. The compiler enforces a set of rules at compile time to guarantee:
- No use-after-free
- No double-free
- No data races
- No dangling pointers
All of that, before your program ever runs.
The Three Ownership Rules
Rule 1: Each value in Rust has a single owner
Every value has exactly one variable that owns it.
let s = String::from("hello");
Here:
- The
Stringvalue"hello"is owned bys sis responsible for freeing the memory when it goes out of scope
Rule 2: When the owner goes out of scope, the value is dropped
When a variable goes out of scope, Rust automatically calls drop.
{
let s = String::from("hello");
} // s goes out of scope → memory is freed
No free(), no GC. Rust does this deterministically.
Rule 3: A value can only have one owner at a time
This is the rule that surprises most people.
let s1 = String::from("hello");
let s2 = s1; // ownership moves
After this:
s2owns the strings1is invalid
println!("{}", s1); // ❌ compile-time error
Why?
Because if both s1 and s2 could free the same memory, you’d get a double free bug.
Rust prevents that by moving ownership instead of copying.
Move vs Copy
Types that are moved
Heap-allocated types: String, Vec<T>, Box<T>
let v1 = vec![1, 2, 3];
let v2 = v1; // move
// v1 is no longer usable
Types that are copied
Simple stack-only types implement the Copy trait:
i32,f64boolchar- Tuples of
Copytypes
let x = 5;
let y = x; // copy
println!("{}", x); // ✅ OK
These are cheap to duplicate and don’t manage heap memory.
Ownership and Functions
Passing ownership to a function
fn take_ownership(s: String) {
println!("{}", s);
} // s is dropped here
let s = String::from("hello");
take_ownership(s);
println!("{}", s); // ❌ error
Ownership moves into the function.
Returning ownership
fn give_back(s: String) -> String {
s
}
let s1 = String::from("hello");
let s2 = give_back(s1);
// s1 is invalid, s2 owns the string
This works, but passing ownership back and forth gets annoying.
That’s where borrowing comes in.
Borrowing (References)
Instead of transferring ownership, you can borrow a value.
fn print_length(s: &String) {
println!("{}", s.len());
}
let s = String::from("hello");
print_length(&s);
println!("{}", s); // ✅ still valid
&Stringis an immutable reference- The function can read but not modify
- Ownership never changes
Mutable Borrowing
You can also borrow mutably—but carefully.
fn add_world(s: &mut String) {
s.push_str(" world");
}
let mut s = String::from("hello");
add_world(&mut s);
println!("{}", s); // "hello world"
The Borrowing Rules (Very Important)
At any given time, either:
- Any number of immutable references
let r1 = &s;
let r2 = &s;
OR
- Exactly one mutable reference
let r = &mut s;
❌ You cannot mix them.
let r1 = &s;
let r2 = &mut s; // ❌ compile-time error
Why?
Because mixing mutable and immutable access could lead to data races.
Rust enforces this at compile time.
Dangling References (Prevented by Ownership)
Rust won’t let references outlive the data they point to.
fn dangling() -> &String {
let s = String::from("hello");
&s // ❌ s is dropped at end of function
}
The compiler stops this entirely.
A Complete Example: Ownership in Action
fn main() {
let mut s = String::from("Rust");
let len = calculate_length(&s);
println!("Length: {}", len);
modify(&mut s);
println!("Modified: {}", s);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
fn modify(s: &mut String) {
s.push_str(" is awesome");
}
sowns theStringcalculate_lengthborrows immutably- Ownership stays in
main modifyborrows mutably- Only one mutable borrow exists at that time
- Memory is freed automatically when main ends
Why Ownership Matters
Ownership lets Rust guarantee:
- No null pointers
- No use-after-free
- No data races
- No memory leaks (unless you explicitly opt in)
All with zero runtime cost.